<?php 

namespace pmvc\aop;

use pmvc\util\ClassUtil;
use Exception;
use ReflectionClass;
use ReflectionMethod;

/**
 * Utility class for creating {@link Proxy}s.
 */
class ProxyUtil {

	private static $DYNA_CLASSES = Array();

	/**
	 * Checks whether or not the given object is
	 * a proxy.
	 * @param mixed $obj
	 */
	public static function isProxy($obj) {
		return ClassUtil::isInstance($obj, 'pmvc\aop\Proxy');
	}

	/**
	 * Recurses up a tree of proxy objects until finding
	 * the root proxied object.
	 * @param mixed $obj
	 */
	public static function getProxiedObject($obj) {
		if (!self::isProxy($obj)) {
			return $obj;
		} else {
			return self::getProxiedObject($obj->getProxiedObject());
		}
	}

	/**
	 * Creates a dynamic proxy implementing the given interfaces.
	 * @param string $interfaces
	 * @param MethodInterceptor $methodInterceptor
	 * @param mixed $userData
	 * @param object $object
	 */
	public static function dynamicProxy(
		MethodInterceptor $methodInterceptor,
		PropertyInterceptor $propertyInterceptor=NULL, 
		$interfaces=Array(), $object=NULL, $userData=NULL) {

		// if we don't have interfaces or an object
		// then throw an exception
		$hasInterfaces	= isset($interfaces) && $interfaces!=NULL && count($interfaces)>0;
		$hasObject		= isset($object) && $object!=NULL;
		if (!$hasInterfaces && !$hasObject) {
			throw new \Exception("Must provided at least interfaces or an object");

		// make sure they're not trying to create a PropertyInterceptor
		// for a non concrete object
		} else if (!$hasObject && $propertyInterceptor!=NULL) {
			throw new \Exception("Can't use a PropertyInterceptor when Proxying a non concrete class");

		// extract interface names
		} else if (!$hasInterfaces && $hasObject) {
			$clazz = ClassUtil::loadClass(get_class($object));
			$interfaces = $clazz->getInterfaceNames();

		}

		// create a unique list of interface names
		$interfaces = array_unique($interfaces);
		sort($interfaces);

		// generate class name
		$className = "PMVCDynaProxy_";
		if ($hasObject) {
			$part = preg_replace('/.*[\\\]/', '', get_class($object));
			$part = preg_replace('/[^A-Za-z0-9_]/', '_', $part);
			$className .= $part."_";
		}
		foreach ($interfaces as $interface) {
			$part = preg_replace('/.*[\\\]/', '', $interface);
			$part = preg_replace('/[^A-Za-z0-9_]/', '_', $part);
			$className .= "__".$part;
		}

		// is it already loaded?
		if (!array_key_exists($className, self::$DYNA_CLASSES)
			&& class_exists($className, FALSE)) {
			self::$DYNA_CLASSES[$className] = ClassUtil::loadClass($className);

		// create class if needed
		} else if (!array_key_exists($className, self::$DYNA_CLASSES)) {
			\pmvc\log\Log::debug("Creating class: {0}", $className);

			// delcare class
			$code = "";
			$code .= "class ".$className." extends pmvc\\aop\\AbstractDynamicProxy implements ".join(", ", $interfaces)." {\n";

			// add constructor
			$code .= "	public function __construct(\$m, \$p, \$userData, \$object, \$interfaces) {\n";
			$code .= "		parent::__construct(\$m, \$p, \$userData, \$object, \$interfaces);\n";
			$code .= "	}\n";

			// add method declarations
			foreach ($interfaces as $interface) {
				$clazz = ClassUtil::loadClass($interface);
				if (!$clazz->isInterface()) {
					throw new Exception($interface." is not an interface");
				}
				$code .= self::buildMethodDeclarations($clazz, TRUE);
			}

			$code .= "}\n";

			// compile and cache
			if (eval($code)===FALSE) {
				throw new Exception("Error creating code for dynamic proxy");
			}
			self::$DYNA_CLASSES[$className] = ClassUtil::loadClass($className);
		}

		// create instance
		return self::$DYNA_CLASSES[$className]->newInstanceArgs(
			Array($methodInterceptor, $propertyInterceptor, $userData, $object, $interfaces));
	}

	private static function buildMethodDeclarations(ReflectionClass $clazz, $includeParent=FALSE) {
		$code = "// ".$clazz->getName()." methods\n";
		foreach ($clazz->getMethods() as $method) {
			$code .= self::buildMethodDeclaration($method) . " {\n";
			$code .= "	return \$this->m->methodInvoked(\$this->object, \"".$method->getName()."\", func_get_args(), \$this->userData);\n";
			$code .= "}\n";
		}
		if ($includeParent && $clazz->getParentClass()!=NULL) {
			$code .= buildMethodDeclarations($clazz->getParentClass(), $includeParent);
		}
		return $code;
	}

	private static function buildMethodDeclaration(ReflectionMethod $method) {
		$code = "";
		$params = $method->getParameters();
		$code = "public function ".$method->getName()."(";
		for ($i=0; $i<count($params); $i++) {

			// get param
			$param = $params[$i];

			// add type
			if ($param->getClass()!=NULL) {
				$code .= $param->getClass()->getName()." ";
			} else if ($param->isArray()) {
				$code .= "Array ";
			}

			// pass by reference
			if ($param->isPassedByReference()) {
				$code .= "&";
			}

			// add name
			$code .= "\$".$param->getName();

			// add default value
			if ($param->isDefaultValueAvailable()) {
				$code .= "=".$param->getDefaultValue();
			}

			// coma?
			if ($i<(count($params)-1)) {
				$code .= ", ";
			}
		}

		// close paren
		$code .= ")";
		return $code;
	}

}

abstract class AbstractDynamicProxy
	implements Proxy {
	protected $m 			= NULL;
	protected $p 			= NULL;
	protected $userData 	= NULL;
	protected $object 		= NULL;
	protected $interfaces	= NULL;
	public function __construct($m=NULL, $p=NULL, $userData=NULL, $object=NULL, $interfaces=NULL) {
		$this->m 			= $m;
		$this->p 			= $p;
		$this->userData 	= $userData;
		$this->object 		= $object;
		$this->interfaces 	= $interfaces;
	}
	public function getProxiedObject() {
		return $this->object;
	}
	public function __call($methodName, $args) {
		if (!empty($this->m)) {
			return $this->m->methodInvoked(
				$this->object, $methodName, $args, $this->userData);
		} else {
			return call_user_func_array(array($methodName, $this->object), $args);
		}
	}
	public function __set($name, $value) {
		if (!empty($this->p)) {
			$this->p->set(
				$this->object, $name, $value, $this->userData);
		} else {
			$this->object->$name = $value;
		}
	}
	public function __get($name) {
		if (!empty($this->p)) {
			return $this->p->get(
				$this->object, $name, $this->userData);
		} else {
			return $this->object->$name;
		}
	}
	public function __isset($name) {
		if (!empty($this->p)) {
			return $this->p->is_set(
				$this->object, $name, $this->userData);
		} else {
			return isset($this->object->$name);
		}
	}
	public function __unset($name) {
		if (!empty($this->p)) {
			$this->p->un_set(
				$this->object, $name, $this->userData);
		} else {
			unset($this->object->$name);
		}
	}
}

?>